package PDF::API2::Resource::CIDFont::TrueType::FontFile;

use base 'PDF::API2::Basic::PDF::Dict';

use strict;
use warnings;

our $VERSION = '2.042'; # VERSION

use Carp;
use Encode qw(:all);
use Font::TTF::Font;
use POSIX qw(ceil floor);

use PDF::API2::Util;
use PDF::API2::Basic::PDF::Utils;

our $cmap = {};

sub _look_for_cmap {
    my $map = shift();
    my $filename = lc($map);
    $filename =~ s/[^a-z0-9]+//gi;
    return { %{$cmap->{$filename}} } if defined $cmap->{$filename};
    eval "require 'PDF/API2/Resource/CIDFont/CMap/$filename.cmap'";
    unless ($@) {
        return { %{$cmap->{$filename}} };
    }
    else {
        die "requested cmap '$map' not installed";
    }
}

sub readcffindex {
    my ($fh, $off) = @_;
    my @idx;
    my $index = [];
    my $buf;
    seek($fh, $off, 0);
    read($fh, $buf, 3);
    my ($count, $offsize) = unpack('nC', $buf);
    foreach (0 .. $count) {
        read($fh, $buf, $offsize);
        $buf = substr("\x00\x00\x00$buf", -4, 4);
        my $id = unpack('N', $buf);
        push @idx, $id;
    }
    my $dataoff = tell($fh) - 1;

    foreach my $i (0 .. $count - 1) {
        push @{$index}, {
            'OFF' => $dataoff + $idx[$i],
            'LEN' => $idx[$i + 1] - $idx[$i],
        };
    }
    return $index;
}

sub readcffdict {
    my ($fh, $off, $len, $foff, $buf) = @_;
    my @idx;
    my $dict = {};
    seek($fh, $off, 0);
    my @st;
    while (tell($fh) < ($off + $len)) {
        read($fh, $buf, 1);
        my $b0 = unpack('C', $buf);
        my $v = '';

        if ($b0 == 12) { # two byte commands
            read($fh, $buf, 1);
            my $b1 = unpack('C', $buf);
            if ($b1 == 0) {
                $dict->{'Copyright'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b1 == 1) {
                $dict->{'isFixedPitch'} = splice(@st, -1);
            }
            elsif ($b1 == 2) {
                $dict->{'ItalicAngle'} = splice(@st, -1);
            }
            elsif ($b1 == 3) {
                $dict->{'UnderlinePosition'} = splice(@st, -1);
            }
            elsif ($b1 == 4) {
                $dict->{'UnderlineThickness'} = splice(@st, -1);
            }
            elsif ($b1 == 5) {
                $dict->{'PaintType'} = splice(@st, -1);
            }
            elsif ($b1 == 6) {
                $dict->{'CharstringType'} = splice(@st,-1);
            }
            elsif ($b1 == 7) {
                $dict->{'FontMatrix'} = [ splice(@st, -4) ];
            }
            elsif ($b1 == 8) {
                $dict->{'StrokeWidth'} = splice(@st, -1);
            }
            elsif ($b1 == 20) {
                $dict->{'SyntheticBase'} = splice(@st, -1);
            }
            elsif ($b1 == 21) {
                $dict->{'PostScript'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b1 == 22) {
                $dict->{'BaseFontName'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b1 == 23) {
                $dict->{'BaseFontBlend'} = [ splice(@st, 0) ];
            }
            elsif ($b1 == 24) {
                $dict->{'MultipleMaster'} = [ splice(@st, 0) ];
            }
            elsif ($b1 == 25) {
                $dict->{'BlendAxisTypes'} = [ splice(@st, 0) ];
            }
            elsif ($b1 == 30) {
                $dict->{'ROS'} = [ splice(@st, -3) ];
            }
            elsif ($b1 == 31) {
                $dict->{'CIDFontVersion'} = splice(@st, -1);
            }
            elsif ($b1 == 32) {
                $dict->{'CIDFontRevision'} = splice(@st, -1);
            }
            elsif ($b1 == 33) {
                $dict->{'CIDFontType'} = splice(@st, -1);
            }
            elsif ($b1 == 34) {
                $dict->{'CIDCount'} = splice(@st, -1);
            }
            elsif ($b1 == 35) {
                $dict->{'UIDBase'} = splice(@st, -1);
            }
            elsif ($b1 == 36) {
                $dict->{'FDArray'} = { 'OFF' => $foff + splice(@st, -1) };
            }
            elsif ($b1 == 37) {
                $dict->{'FDSelect'} = { 'OFF' => $foff + splice(@st, -1) };
            }
            elsif ($b1 == 38) {
                $dict->{'FontName'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b1 == 39) {
                $dict->{'Chameleon'} = splice(@st, -1);
            }
            next;
        }
        elsif ($b0 < 28) { # commands
            if ($b0 == 0) {
                $dict->{'Version'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b0 == 1) {
                $dict->{'Notice'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b0 == 2) {
                $dict->{'FullName'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b0 == 3) {
                $dict->{'FamilyName'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b0 == 4) {
                $dict->{'Weight'} = { 'SID' => splice(@st, -1) };
            }
            elsif ($b0 == 5) {
                $dict->{'FontBBX'} = [ splice(@st, -4) ];
            }
            elsif ($b0 == 13) {
                $dict->{'UniqueID'} = splice(@st, -1);
            }
            elsif ($b0 == 14) {
                $dict->{'XUID'} = [splice(@st, 0)];
            }
            elsif ($b0 == 15) {
                $dict->{'CharSet'} = { 'OFF' => $foff + splice(@st, -1) };
            }
            elsif ($b0 == 16) {
                $dict->{'Encoding'} = { 'OFF' => $foff + splice(@st, -1) };
            }
            elsif ($b0 == 17) {
                $dict->{'CharStrings'} = { 'OFF' => $foff + splice(@st, -1) };
            }
            elsif ($b0 == 18) {
                $dict->{'Private'} = {
                    'LEN' => splice(@st, -1),
                    'OFF' => $foff + splice(@st, -1),
                };
            }
            next;
        }
        elsif ($b0 == 28) { # int16
            read($fh, $buf, 2);
            $v = unpack('n!', $buf);
        }
        elsif ($b0 == 29) { # int32
            read($fh, $buf, 4);
            $v = unpack('N!', $buf);
        }
        elsif ($b0 == 30) { # float
            my $e = 1;
            while ($e) {
                read($fh, $buf, 1);
                my $v0 = unpack('C', $buf);
                foreach my $m ($v0 >> 8, $v0 & 0xf) {
                    if ($m < 10) {
                        $v .= $m;
                    }
                    elsif ($m == 10) {
                        $v .= '.';
                    }
                    elsif ($m == 11) {
                        $v .= 'E+';
                    }
                    elsif ($m == 12) {
                        $v .= 'E-';
                    }
                    elsif ($m == 14) {
                        $v .= '-';
                    }
                    elsif ($m == 15) {
                        $e = 0;
                        last;
                    }
                }
            }
        }
        elsif ($b0 == 31) { # command
            $v = "c=$b0";
            next;
        }
        elsif ($b0 < 247) { # 1 byte signed
            $v = $b0 - 139;
        }
        elsif ($b0 < 251) { # 2 byte plus
            read($fh, $buf, 1);
            $v = unpack('C', $buf);
            $v = ($b0 - 247) * 256 + ($v + 108);
        }
        elsif ($b0 < 255) { # 2 byte minus
            read($fh, $buf, 1);
            $v = unpack('C', $buf);
            $v = -($b0 - 251) * 256 - $v - 108;
        }
        push @st, $v;
    }

    return $dict;
}

sub read_kern_table {
    my ($font, $upem, $self) = @_;
    my $fh = $font->{' INFILE'};
    my $data;
    my $buf;

    return unless $font->{'kern'};

    seek($fh, $font->{'kern'}->{' OFFSET'} + 2, 0);
    read($fh, $buf, 2);
    my $num = unpack('n', $buf);
    foreach my $n (1 .. $num) {
        read($fh, $buf, 6);
        my ($ver, $len, $cov) = unpack('n3', $buf);
        $len -= 6;
        my $fmt = $cov >> 8;
        if ($fmt == 0) {
            $data ||= {};
            read($fh, $buf, 8);
            my $nc = unpack('n', $buf);
            foreach (1 .. $nc) {
                read($fh, $buf, 6);
                my ($idx1, $idx2, $val) = unpack('n2n!', $buf);
                if ($val < 0) {
                    $val = -floor($val * 1000 / $upem);
                }
                else {
                    $val = -ceil($val * 1000 / $upem);
                }
                if ($val != 0) {
                    $data->{"$idx1:$idx2"} = $val;
                    $data->{join(':',
                                 $self->data->{'g2n'}->[$idx1],
                                 $self->data->{'g2n'}->[$idx2])} = $val;
                }
            }
        }
        elsif ($fmt == 2) {
            read($fh, $buf, $len);
        }
        else {
            read($fh, $buf, $len);
        }
    }
    return $data;
}

sub readcffstructs {
    my $font = shift();
    my $fh = $font->{' INFILE'};
    my $data = {};

    # read CFF table
    seek($fh, $font->{'CFF '}->{' OFFSET'}, 0);
    my $buf;
    read($fh, $buf, 4);
    my ($cffmajor, $cffminor, $cffheadsize, $cffglobaloffsize) = unpack('C4', $buf);

    $data->{'name'} = readcffindex($fh, $font->{'CFF '}->{' OFFSET'} + $cffheadsize);
    foreach my $dict (@{$data->{'name'}}) {
        seek($fh, $dict->{'OFF'}, 0);
        read($fh, $dict->{'VAL'}, $dict->{'LEN'});
    }

    $data->{'topdict'} = readcffindex($fh, $data->{'name'}->[-1]->{'OFF'} + $data->{'name'}->[-1]->{'LEN'});
    foreach my $dict (@{$data->{'topdict'}}) {
        $dict->{'VAL'} = readcffdict($fh, $dict->{'OFF'}, $dict->{'LEN'}, $font->{'CFF '}->{' OFFSET'});
    }

    $data->{'string'} = readcffindex($fh, $data->{'topdict'}->[-1]->{'OFF'} + $data->{'topdict'}->[-1]->{'LEN'});
    foreach my $dict (@{$data->{'string'}}) {
        seek($fh, $dict->{'OFF'}, 0);
        read($fh, $dict->{'VAL'}, $dict->{'LEN'});
    }
    push @{$data->{'string'}}, { 'VAL' => '001.000' };
    push @{$data->{'string'}}, { 'VAL' => '001.001' };
    push @{$data->{'string'}}, { 'VAL' => '001.002' };
    push @{$data->{'string'}}, { 'VAL' => '001.003' };
    push @{$data->{'string'}}, { 'VAL' => 'Black' };
    push @{$data->{'string'}}, { 'VAL' => 'Bold' };
    push @{$data->{'string'}}, { 'VAL' => 'Book' };
    push @{$data->{'string'}}, { 'VAL' => 'Light' };
    push @{$data->{'string'}}, { 'VAL' => 'Medium' };
    push @{$data->{'string'}}, { 'VAL' => 'Regular' };
    push @{$data->{'string'}}, { 'VAL' => 'Roman' };
    push @{$data->{'string'}}, { 'VAL' => 'Semibold' };

    foreach my $dict (@{$data->{'topdict'}}) {
        foreach my $k (keys %{$dict->{'VAL'}}) {
            my $dt = $dict->{'VAL'}->{$k};
            if ($k eq 'ROS') {
                $dict->{'VAL'}->{$k}->[0] = $data->{'string'}->[$dict->{'VAL'}->{$k}->[0] - 391]->{'VAL'};
                $dict->{'VAL'}->{$k}->[1] = $data->{'string'}->[$dict->{'VAL'}->{$k}->[1] - 391]->{'VAL'};
                next;
            }
            next unless ref($dt) eq 'HASH' and defined($dt->{'SID'});
            if ($dt->{'SID'} >= 379) {
                $dict->{'VAL'}->{$k} = $data->{'string'}->[$dt->{'SID'} - 391]->{'VAL'};
            }
        }
    }
    my $dict = {};
    foreach my $k (qw(CIDCount CIDFontVersion FamilyName FontBBX FullName ROS Weight XUID)) {
        $dict->{$k} = $data->{'topdict'}->[0]->{'VAL'}->{$k} if defined $data->{'topdict'}->[0]->{'VAL'}->{$k};
    }
    return $dict;
}

sub new {
    my ($class, $pdf, $file, %opts) = @_;
    my $data = {};

    confess "cannot find font '$file'" unless -f $file;
    my $font = Font::TTF::Font->open($file);
    $data->{'obj'} = $font;

    $class = ref($class) if ref($class);
    my $self = $class->SUPER::new();

    $self->{'Filter'} = PDFArray(PDFName('FlateDecode'));
    $self->{' font'} = $font;
    $self->{' data'} = $data;

    $data->{'noembed'} = $opts{'embed'} ? 0 : 1;
    $data->{'iscff'} = defined($font->{'CFF '}) ? 1 : 0;

    $self->{'Subtype'} = PDFName('CIDFontType0C') if $data->{'iscff'};

    $data->{'fontfamily'} = $font->{'name'}->read->find_name(1);
    $data->{'fontname'} = $font->{'name'}->read->find_name(4);

    $font->{'OS/2'}->read();
    my @stretch = qw(
        Normal
        UltraCondensed
        ExtraCondensed
        Condensed
        SemiCondensed
        Normal
        SemiExpanded
        Expanded
        ExtraExpanded
        UltraExpanded
    );
    $data->{'fontstretch'} = $stretch[$font->{'OS/2'}->{'usWidthClass'}] || 'Normal';

    $data->{'fontweight'} = $font->{'OS/2'}->{'usWeightClass'};

    $data->{'panose'} = pack('n', $font->{'OS/2'}->{'sFamilyClass'});

    foreach my $p (qw[bFamilyType bSerifStyle bWeight bProportion bContrast bStrokeVariation bArmStyle bLetterform bMidline bXheight]) {
        $data->{'panose'} .= pack('C', $font->{'OS/2'}->{$p});
    }

    $data->{'apiname'} = join('', map { ucfirst(lc(substr($_, 0, 2))) } split m/[^A-Za-z0-9\s]+/, $data->{'fontname'});
    $data->{'fontname'} =~ s/[\x00-\x1f\s]//g;

    $data->{'altname'} = $font->{'name'}->find_name(1);
    $data->{'altname'} =~ s/[\x00-\x1f\s]//g;

    $data->{'subname'} = $font->{'name'}->find_name(2);
    $data->{'subname'} =~ s/[\x00-\x1f\s]//g;

    $font->{'cmap'}->read->find_ms();
    if (defined $font->{'cmap'}->find_ms()) {
        $data->{'issymbol'} = ($font->{'cmap'}->find_ms->{'Platform'} == 3 and $font->{'cmap'}->read->find_ms->{'Encoding'} == 0) || 0;
    }
    else {
        $data->{'issymbol'} = 0;
    }

    $data->{'upem'} = $font->{'head'}->read->{'unitsPerEm'};

    $data->{'fontbbox'} = [
        int($font->{'head'}->{'xMin'} * 1000 / $data->{'upem'}),
        int($font->{'head'}->{'yMin'} * 1000 / $data->{'upem'}),
        int($font->{'head'}->{'xMax'} * 1000 / $data->{'upem'}),
        int($font->{'head'}->{'yMax'} * 1000 / $data->{'upem'}),
    ];

    $data->{'stemv'} = 0;
    $data->{'stemh'} = 0;

    $data->{'missingwidth'} = int($font->{'hhea'}->read->{'advanceWidthMax'} * 1000 / $data->{'upem'}) || 1000;
    $data->{'maxwidth'} = int($font->{'hhea'}->{'advanceWidthMax'} * 1000 / $data->{'upem'});
    $data->{'ascender'} = int($font->{'hhea'}->read->{'Ascender'} * 1000 / $data->{'upem'});
    $data->{'descender'} = int($font->{'hhea'}{'Descender'} * 1000 / $data->{'upem'});

    $data->{'flags'} = 0;
    $data->{'flags'} |= 1 if $font->{'OS/2'}->read->{'bProportion'} == 9;
    $data->{'flags'} |= 2 unless $font->{'OS/2'}{'bSerifStyle'} > 10 and $font->{'OS/2'}{'bSerifStyle'} < 14;
    $data->{'flags'} |= 8 if $font->{'OS/2'}{'bFamilyType'} == 2;
    $data->{'flags'} |= 32; # if $font->{'OS/2'}{'bFamilyType'} > 3;
    $data->{'flags'} |= 64 if $font->{'OS/2'}{'bLetterform'} > 8;

    $data->{'capheight'} = $font->{'OS/2'}->{'CapHeight'} || int($data->{'fontbbox'}->[3] * 0.8);
    $data->{'xheight'} = $font->{'OS/2'}->{'xHeight'} || int($data->{'fontbbox'}->[3] * 0.4);

    if ($data->{'issymbol'}) {
        $data->{'e2u'} = [0xf000 .. 0xf0ff];
    }
    else {
        $data->{'e2u'} = [unpack('U*', decode('cp1252', pack('C*', 0 .. 255)))];
    }

    if ($font->{'post'}->read->{'FormatType'} == 3 and defined $font->{'cmap'}->read->find_ms()) {
        $data->{'g2n'} = [];
        foreach my $u (sort { $a <=> $b } keys %{$font->{'cmap'}->read->find_ms->{'val'}}) {
            my $n = nameByUni($u);
            $data->{'g2n'}->[$font->{'cmap'}->read->find_ms->{'val'}->{$u}] = $n;
        }
    }
    else {
        $data->{'g2n'} = [ map { $_ || '.notdef' } @{$font->{'post'}->read->{'VAL'}} ];
    }

    $data->{'italicangle'} = $font->{'post'}->{'italicAngle'};
    $data->{'isfixedpitch'} = $font->{'post'}->{'isFixedPitch'};
    $data->{'underlineposition'} = $font->{'post'}->{'underlinePosition'};
    $data->{'underlinethickness'} = $font->{'post'}->{'underlineThickness'};

    if ($self->iscff()) {
        $data->{'cff'} = readcffstructs($font);
    }

    if (defined $data->{'cff'}->{'ROS'}) {
        my %cffcmap = (
            'Adobe:Japan1' => 'japanese',
            'Adobe:Korea1' => 'korean',
            'Adobe:CNS1' => 'traditional',
            'Adobe:GB1' => 'simplified',
        );
        my $key = $data->{'cff'}->{'ROS'}->[0] . ':' . $data->{'cff'}->{'ROS'}->[1];
        my $ccmap = _look_for_cmap($cffcmap{$key} // $key);
        $data->{'u2g'} = $ccmap->{'u2g'};
        $data->{'g2u'} = $ccmap->{'g2u'};
    }
    else {
        $data->{'u2g'} = {};

        my $gmap = $font->{'cmap'}->read->find_ms->{'val'};
        foreach my $u (sort {$a <=> $b} keys %$gmap) {
            my $uni = $u || 0;
            $data->{'u2g'}->{$uni} = $gmap->{$uni};
        }
        $data->{'g2u'} = [ map { $_ || 0 } $font->{'cmap'}->read->reverse() ];
    }

    if ($data->{'issymbol'}) {
        map { $data->{'u2g'}->{$_}        ||= $font->{'cmap'}->read->ms_lookup($_) } (0xf000 .. 0xf0ff);
        map { $data->{'u2g'}->{$_ & 0xff} ||= $font->{'cmap'}->read->ms_lookup($_) } (0xf000 .. 0xf0ff);
    }

    $data->{'e2n'} = [ map { $data->{'g2n'}->[$data->{'u2g'}->{$_} || 0] || '.notdef' } @{$data->{'e2u'}} ];

    $data->{'e2g'} = [ map { $data->{'u2g'}->{$_ || 0} || 0 } @{$data->{'e2u'}} ];
    $data->{'u2e'} = {};
    foreach my $n (reverse 0 .. 255) {
        $data->{'u2e'}->{$data->{'e2u'}->[$n]} //= $n;
    }

    $data->{'u2n'} = { map { $data->{'g2u'}->[$_] => $data->{'g2n'}->[$_] } (0 .. (scalar @{$data->{'g2u'}} - 1)) };

    $data->{'wx'} = [];
    foreach my $i (0 .. (scalar @{$data->{'g2u'}} - 1)) {
        my $hmtx = $font->{'hmtx'}->read->{'advance'}->[$i];
        if ($hmtx) {
            $data->{'wx'}->[$i] = int($hmtx * 1000 / $data->{'upem'});
        }
        else {
            $data->{'wx'}->[$i] = $data->{'missingwidth'};
        }
    }

    $data->{'kern'} = read_kern_table($font, $data->{'upem'}, $self);
    delete $data->{'kern'} unless defined $data->{'kern'};

    $data->{'fontname'}   =~ s/\s+//g;
    $data->{'fontfamily'} =~ s/\s+//g;
    $data->{'apiname'}    =~ s/\s+//g;
    $data->{'altname'}    =~ s/\s+//g;
    $data->{'subname'}    =~ s/\s+//g;

    $self->subsetByCId(0);

    return ($self, $data);
}

sub font { return $_[0]->{' font'} }
sub data { return $_[0]->{' data'} }
sub iscff { return $_[0]->data->{'iscff'} }

sub haveKernPairs { return $_[0]->data->{'kern'} ? 1 : 0 }

sub kernPairCid {
    my ($self, $i1, $i2) = @_;
    return 0 if $i1 == 0 and $i2 == 0;
    return $self->data->{'kern'}->{"$i1:$i2"} || 0;
}

sub subsetByCId {
    my ($self, $g) = @_;
    $self->data->{'subset'} = 1;
    vec($self->data->{'subvec'}, $g, 1) = 1;
    return if $self->iscff();
    if (defined $self->font->{'loca'}->read->{'glyphs'}->[$g]) {
        $self->font->{'loca'}->read->{'glyphs'}->[$g]->read();
        foreach my $ref ($self->font->{'loca'}->{'glyphs'}->[$g]->get_refs()) {
            vec($self->data->{'subvec'}, $ref, 1) = 1;
        }
    }
}

sub subvec {
    my $self = shift();
    return 1 if $self->iscff();
    my $g = shift();
    return vec($self->data->{'subvec'}, $g, 1);
}

sub glyphNum {
    my $self = shift();
    return $self->font->{'maxp'}->read->{'numGlyphs'};
}

sub outobjdeep {
    my ($self, $fh, $pdf) = @_;

    my $f = $self->font();

    if ($self->iscff()) {
        $f->{'CFF '}->read_dat();
        $self->{' stream'} = $f->{'CFF '}->{' dat'};
    }
    else {
        if ($self->data->{'subset'} and not $self->data->{'nosubset'}) {
            $f->{'glyf'}->read();
            for (my $i = 0; $i < $self->glyphNum(); $i++) {
                next if $self->subvec($i);
                $f->{'loca'}{'glyphs'}->[$i] = undef;
            }
        }
        unless ($self->data->{'noembed'}) {
            $self->{' stream'} = '';
            my $ffh;
            CORE::open($ffh, '+>', \$self->{' stream'});
            binmode($ffh, ':raw');
            $f->out($ffh, 'cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'prep');
            $self->{'Length1'} = PDFNum(length($self->{' stream'}));
            CORE::close($ffh);
        }
    }

    $self->SUPER::outobjdeep($fh, $pdf);
}

1;
